# init
在将路由实例挂载到 Vue 实例后,会执行 init 函数。
核心源码如下:
VueRouter.prototype.init = function init (app /* Vue component instance */) {
// ...
var history = this.history;
if (history instanceof HTML5History || history instanceof HashHistory) {
var handleInitialScroll = function (routeOrError) {
var from = history.current;
var expectScroll = this$1.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this$1, routeOrError, from, false);
}
};
var setupListeners = function (routeOrError) {
history.setupListeners();
handleInitialScroll(routeOrError);
};
// 调用 transitionTo 进行路由的跳转
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
);
}
// 将 app._route 设置为当前路由
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
// ...
}
该函数的核心逻辑是:
- 跳转到目标路由,主要调用
history.transitionTo
函数。 - 改变路由的 URL 之后,将 app._route 设置为当前路由,由于在初始化的时候已经通过将 _route 设置为响应式的,所以一旦 _route 改变,对应的视图也会进行变更。
# transitionTo
该函数的核心源码如下:
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route;
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
// 通过当前 location 匹配到当前路由信息
route = this.router.match(location, this.current);
} catch (e) {
// 错误处理
this.errorCbs.forEach(function (cb) {
cb(e);
});
throw e
}
var prev = this.current;
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();
this$1.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},
function (err) {
if (onAbort) {
onAbort(err);
}
if (err && !this$1.ready) {
// 错误处理
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {
cb(err);
});
}
}
}
);
};
该函数接收函数参数:
- 当前的 location:通过当前 location 匹配到当前路由信息;
- onComplete:执行成功回调;
- onAbort:执行失败回调。
接着,该函数内部的主要核心是调用confirmTransition
。
# confirmTransition
该函数主要源码如下:
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var this$1 = this;
var current = this.current;
this.pending = route; // 保存当前路由
var abort = function (err) {
// 错误处理函数
};
var lastRouteIndex = route.matched.length - 1; // 上个路由在路由配置表中的索引
var lastCurrentIndex = current.matched.length - 1; // 当前路由在路由配置表中的索引
// 判断是否是同一个路由
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL();
if (route.hash) {
handleScroll(this.router, current, route, false);
}
return abort(createNavigationDuplicatedError(current, route))
}
// 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
var ref = resolveQueue(
this.current.matched,
route.matched
);
var updated = ref.updated; // 可复用的组件
var deactivated = ref.deactivated; // 失活的组件
var activated = ref.activated; // 需要重新渲染的组件
// 路由守卫数组
var queue = [].concat(
// 组件内离开路由守卫
extractLeaveGuards(deactivated),
// 全局离开钩子
this.router.beforeHooks,
// 组件内更新钩子
extractUpdateHooks(updated),
// 需要渲染组件 enter 守卫钩子
activated.map(function (m) { return m.beforeEnter; }),
// 处理异步组件
resolveAsyncComponents(activated)
);
// runQueue 第二个参数,主要用于接收路由守卫并执行对应的钩子函数
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current,
function (to) {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(createNavigationAbortedError(current, route));
} else if (isError(to)) {
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route));
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
// 执行下一次迭代
next(to);
}
});
} catch (e) {
abort(e);
}
};
// 通过 runQueue 是实现异步函数同步化执行逻辑
runQueue(queue, iterator, function () {
// 执行 enter 路由守卫钩子
var enterGuards = extractEnterGuards(activated);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this$1.pending = null;
onComplete(route); // 执行回调
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
handleRouteEntered(route);
});
}
});
});
};
该函数的主要逻辑:执行一系列路由守卫函数。
runQueue
主要实现异步函数同步化执行:
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
// 迭代结束,执行回调
cb();
} else {
if (queue[index]) {
fn(queue[index], function () {
step(index + 1); // 执行下一个迭代
});
} else {
step(index + 1); // 执行下一个迭代
}
}
};
step(0); // 首次迭代
}
# 路由守卫调用执行逻辑
以组件内离开路由守卫extractLeaveGuards(deactivated)
为例。
首先,进入 extractLeaveGuards 函数,函数参数 deactivated 为通过 resolveQueue 解析出的失活路由列表。
function extractLeaveGuards (deactivated) {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
函数内调用 extractGuards 函数,函数参数为:
- records:路由记录列表。这里是通过 resolveQueue 解析出的失活路由列表 deactivated。records 的列表项结构如下:
interface RouteRecord {
path: string
regex: RegExp
components: Dictionary<Component>
instances: Dictionary<Vue>
name?: string
parent?: RouteRecord
redirect?: RedirectOption
matchAs?: string
meta: RouteMeta
beforeEnter?: (
route: Route,
redirect: (location: RawLocation) => void,
next: () => void
) => any
props:
| boolean
| Object
| RoutePropsFunction
| Dictionary<boolean | Object | RoutePropsFunction>
}
- name:路由守卫名,这里是 beforeRouteLeave。
- bind:将路由守卫的 this 绑定到具体组件实例上的函数,并执行路由守卫。下面是 bindGuard 函数源码:
function bindGuard (guard, instance) {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
- reverse:因为某些钩子函数需要从子执行到父,所以需要判断是否需要反转。
接下来进入 extractGuards 函数:
function extractGuards (
records,
name,
bind,
reverse
) {
var guards = flatMapComponents(records, function (def, instance, match, key) {
var guard = extractGuard(def, name);
if (guard) {
return Array.isArray(guard)
? guard.map(function (guard) { return bind(guard, instance, match, key); })
: bind(guard, instance, match, key)
}
});
return flatten(reverse ? guards.reverse() : guards)
}
extractGuards 函数调用 flatMapComponents 函数,传入路由列表和回调函数,最后返回路由守卫列表 guards。
进入 flatMapComponents 函数:
function flatMapComponents (
matched,
fn
) {
return flatten(matched.map(function (m) { // 遍历路由列表
return Object.keys(m.components).map(function (key) { return fn( // 遍历路由列表项中的每个路由组件
m.components[key],
m.instances[key],
m, key
); })
}))
}
// 二级数组扁平化
function flatten (arr) {
return Array.prototype.concat.apply([], arr)
}
flatMapComponents 函数执行逻辑:遍历路由列表,然后获取每个路由列表项对应的路由组件列表,接着遍历路由组件列表,获取每个路由组件的组件定义和组件实例,最后执行传入的回调函数,该回调函数也就是在 extractGuards 函数中调用 flatMapComponents 传入的第二个参数。回调函数源码如下:
function (def, instance, match, key) {
var guard = extractGuard(def, name);
if (guard) {
return Array.isArray(guard)
? guard.map(function (guard) { return bind(guard, instance, match, key); })
: bind(guard, instance, match, key)
}
}
回调函数内部执行逻辑:提取目标路由守卫函数,将路由守卫的 this 绑定到具体组件实例上的函数,并执行路由守卫。